<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body,ul{
            margin: 0;
            padding: 0;
        }
        ul,ul li{
            list-style-type: none;
        }
        body{
            height: 1800px;
        }
        #list{
            /* height: 150px; */
            /* overflow-x: hidden; */
            width: 400px;
            margin: 0 auto;
            margin-top: 100px;
            border: 1px dashed blue;
            padding: 10px;
            box-sizing: border-box;
        }

        .item{
            padding: 10px;
        }
        .item:not(:last-child){
            margin-bottom: 10px;
        }

        .ani {
            animation: ani 3s ease-in-out 1s both;
        }

        @keyframes ani {
            from {
                color: red;
            }
            to{
                color: blue;
            }
        }

    </style>
</head>
<body>
    <div id="list">
        <ul>
            <li id="item1" class="item" draggable="true" style="background: linear-gradient(to right, red, blue);">1水电费水电费，收到烦死了，乐山大佛</li>
            <li class="item" draggable="true" style="background-color: rgb(18, 206, 96);">2</li>
            <li class="item" draggable="true" style="background-color: rgb(163, 68, 226);">3</li>
            <li class="item" draggable="true" style="background-color: rgb(241, 135, 73);">4</li>
            <li class="item" draggable="true" style="background-color: rgb(56, 129, 240);">5</li>
        </ul>
    </div>
    <script>
        /**
         * 理解目标元素
         * 拖拽事件中 dragStart dragEnd drag。event.target就是被拖拽元素
         * 拖放事件中 dragEnter dragOver drop。event.target就是拖放区域元素
         * 
        */

        /**
         * 为什么dragover要阻止默认事件？
         * dragover事件在拖动元素经过拖放元素时触发，这时候需要阻止默认行为以允许目标元素接收拖放元素。
         * 为什么drop要阻止默认事件？
         * 因为如果拖的是系统文夹，需要在dragover和drop事件中，同时阻止游览器打开文件夹，这一默认行为
         * */
        const items = document.querySelectorAll('.item');

        const list = document.querySelector('#list');

        function each(arr, callback) {
            const keys = Object.keys(arr);
            for(var i = 0; i < keys.length; i++) { 
                if(callback.call(arr, arr[i], i) === false) {
                    break;
                }
            }
        }
        
        function on(els, eventName, callback) {
            if(!Reflect.has(els, 'length')) {
                els = [els]
            }
            each([...els], (el) => el.addEventListener(eventName, callback))
        }

        // vue2中的Flip原理，实现动画
        // https://aerotwist.com/blog/flip-your-animations/
        // animation api的 polyfill: https://github.com/web-animations/web-animations-js
        function flipAni(els) {
            const oldRects = [...els].map((el) => ({ el, rect: el.getBoundingClientRect() }));
            // const wmap = new WeakMap();
            return {
                play() {
                    const lastRects = [...els].map((el) => ({ el, rect: el.getBoundingClientRect() }));
                    each(lastRects, (lastRect, i) => {
                        const { el, rect } = lastRect;
                        const y = oldRects[i].rect.top - rect.top;
                        if(y) {
                            // wmap.has(el) && wmap.get(el).finished.then((animation) => animation.cancel())
                            // console.log('el animations: ', el.getAnimations())
                            const ani = el.animate([
                                {
                                    transform: `translateY(${y}px)`
                                },
                                {
                                    transform: `translateY(0)`
                                }
                            ], {
                                duration: 300,
                                easing: 'cubic-bezier(0,0,0.32,1)',
                            });
                            // wmap.set(el, ani);
                            ani.finished.then(() => oldRects[i].rect = el.getBoundingClientRect())
                        }
                    })
                }
            }
        }

        /**
         * 判断Y轴方向
         * lastY: 最新的y轴坐标。鼠标第一次按下时clientY
         * */ 
        function initDirectionY(lastY) {
            let direction = 'none' // 方向
            return function(curY) {
                curY - lastY > 0 && (direction = 'down')
                curY - lastY < 0 && (direction = 'up')
                // 更新Y坐标
                lastY = curY;
                return direction
            }
        }

        let source = null; // 源元素（当前被拖拽的元素）
        let target = null; // 目标元素（拖放区元素）
        let getDirectionY; // 获取y轴方向的方法
        let directionY = 'none';
        let flip = null;
        // 利用事件委托，子元素触发事件，冒泡到父元素上
        on(list, 'dragstart', (e) => {
            console.log('dragestart', e);
            source = e.target;
            e.dataTransfer.effectAllowed = 'move';
            getDirectionY = initDirectionY(e.clientY);
            flip = flipAni(items);
        })

        // 拖拽时的方法
        // on(items, 'drag', (e) => {});

        on(list, 'dragend', (e) => {
            console.log('dragend', e);
        });

        on(list, 'dragenter', (e) => {
            e.preventDefault();
            if(e.target === source || e.target.getAttribute('draggable') !== "true") {return}
            directionY = getDirectionY(e.clientY)
            target = e.target;
            // 导致重排layout
            if(directionY === 'up') { // 向上
                target.insertAdjacentElement('beforebegin', source);
            }else {
                // 将source新节点，插入到target后面
                target.insertAdjacentElement('afterend', source);
            }
            // 所以， invert值没有问题
            flip.play();
        });

        on(list, 'dragover', (e) => {
            // 一定要禁用
            e.preventDefault();
        })
7
        on(list, 'drop', (e) => {
            // 一定要禁用
            e.preventDefault();
            // console.log('drop', e)
        });
    </script>
</body>
</html>